package com.googlecode.skald.client.affordances;

import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;

import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.googlecode.skald.client.cmds.AffordanceCommand;


/**
 * A node in a Trie structure (also known as a prefix tree).
 * For package-private use while building menus. 
 * 
 * @author Zach Tomaszewski
 */
class TrieNode {
  
  /** Display element */
  String data;
  /** Child elements */
  List<TrieNode> children;
  /** Command text, which should only be set in leaf nodes. */
  String cmd;
  /** Whether this is a weakly afforded action. */
  boolean isWeak;
  
  /** 
   * Builds a new node with the given data and empty children list. 
   * Affordance is not weak.
   */
  public TrieNode(String data) {
    this.data = data;
    this.children = new LinkedList<TrieNode>();
    this.cmd = null;
    this.isWeak = false;
  }
  
  /** 
   * Builds a new node with the given data, empty children list, and 
   * given weakness. 
   */
  public TrieNode(String data, boolean weak) {
    this(data);
    this.isWeak = weak;
  }
  
  /** 
   * Collapses the Trie rooted at this node according to the given opts. 
   * 
   * @see MenuOption
   */
  public void collapse(EnumSet<MenuOption> opts) {
    if (opts.contains(MenuOption.COLLAPSE_VERTICALLY)) {
      for (int i = 0; i < this.children.size(); i++) {
        TrieNode child = this.children.get(i);
        if (i + 1 < this.children.size() && child.cmd == null &&
            child.data.equals(this.children.get(i + 1).data)) {
          //this child is not a cmd and equals next, so combine their children
          assert child.children.size() > 0: 
              child.data + ": Non-cmd-bearing node should have children";
          child.children.addAll(this.children.get(i + 1).children);
          // stays weak only if all options are weak
          child.isWeak = child.isWeak && this.children.get(i + 1).isWeak;
          this.children.remove(i + 1);
          i--; //stay on this child in for loop and process its next sibling
        }else {
          //no more equal siblings, so recurse on this child before continuing
          child.collapse(EnumSet.of(MenuOption.COLLAPSE_VERTICALLY));
        }
      }
    }
    
    if (opts.contains(MenuOption.COLLAPSE_HORIZONTALLY)) {
      List<TrieNode> nodes = new LinkedList<TrieNode>();
      if (this.data == null) { //true root node, so skip over to "real" top lvl
        nodes.addAll(this.children);
      }else {
        nodes.add(this);
      }
      for (TrieNode node : nodes) {        
        if (node.children.size() == 1) {
          TrieNode child = node.children.get(0);
          if (!opts.contains(MenuOption.DO_NOT_COLLAPSE_INTO_TOP_LEVEL)) {
            assert node.cmd == null : node.data + ": non-leaf contains cmd.";        
            //combine with child's data
            node.data += " " + child.data;
            node.children = child.children;
            node.cmd = child.cmd;  //may be null
          }
          //try for grandchildren
          EnumSet<MenuOption> lowerLevel = opts;
          if (lowerLevel.contains(MenuOption.DO_NOT_COLLAPSE_INTO_TOP_LEVEL)) {
            //no longer at top level
            lowerLevel = EnumSet.copyOf(opts);
            lowerLevel.remove(MenuOption.DO_NOT_COLLAPSE_INTO_TOP_LEVEL);
          }else {
            child = node; //child is now absorbed into node
          }
          child.collapse(lowerLevel);  
        }
      }
    }
  }
  
  /** Returns the node containing the given direct child element 
   * or null if the given elem is not a child of this node.
   */
  public TrieNode getChild(String elem) {
    for (TrieNode c : this.children) {
      if (c.data.equals(elem)) {
        return c;
      }
    }
    return null;
  }

  /** Returns whether elem is contained as data in a direct child node. */
  public boolean isChild(String elem) {
    return getChild(elem) == null; 
  }

  /**
   * Returns the data of the given node as a menu item.  If the node has 
   * children, the MenItem will open into a MenuBar filled recursively with 
   * its children as menus.  If the node is a leaf node, the menu item will
   * have an associated command of the given type, which will be built
   * by passing it the associated command text.
   * 
   * @see Affordances#getMenu(List, java.util.EnumSet, Class)
   */
  public static MenuItem toMenuItem(TrieNode trie, 
                                    AffordanceCommand builder) {
    MenuItem item;
    if (trie.children.isEmpty()) {      
      Command cmd = builder.build(trie.cmd);
      item = new MenuItem(trie.data, true, cmd);
    }else {
      item = new MenuItem(trie.data, toMenuBar(trie, builder));
    }
    if (trie.isWeak) {
      item.addStyleName("skald-affordance-weak");
    }
    return item;
  }
  
  /**
   * Returns the children of the given node as a MenuBar.  Each child is 
   * constructed as per {@link #toMenuItem(TrieNode, AffordanceCommand)}.
   * If there are no children, the MenuBar will be empty.
   */
  public static MenuBar toMenuBar(TrieNode trie, 
                                    AffordanceCommand builder) {
    MenuBar menu = new MenuBar(true);
    for (TrieNode child : trie.children) {
      menu.addItem(toMenuItem(child, builder));
    }
    return menu;
  }
  
  
  @Override
  public String toString() {
    return toString(0);
  }

  /**
   * Recursively prints the tree structure using the given indent level. 
   */
  protected String toString(int indent) {
    StringBuffer buff = new StringBuffer();
    buff.append(this.data);
    if (this.cmd != null) {
      buff.append(" [cmd=");
      buff.append(this.cmd);
      buff.append("]");
    }
    buff.append("\n");
    for (TrieNode tn : this.children) {
      for (int i = 0; i < indent; i++) {
        buff.append("  ");
      }
      buff.append("> ");
      buff.append(tn.toString(indent + 1));
    } 
    return buff.toString();
  }
}
